/**
 * Copyright (c) 2010 Basil Shikin, BugStat Project
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.bugstat.exporters.systems.bugzilla;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bugstat.common.bugs.Component;
import org.bugstat.common.bugs.Platform;
import org.bugstat.common.bugs.Priority;
import org.bugstat.common.bugs.Product;
import org.bugstat.common.bugs.Resolution;
import org.bugstat.common.bugs.Severity;
import org.bugstat.common.bugs.Status;
import org.bugstat.common.bugs.User;
import org.bugstat.exporters.BugPermanentInfoWriter;
import org.bugstat.exporters.BugTemporalInfoWriter;
import org.bugstat.exporters.CanExportBugs;
import org.bugstat.exporters.ComponentReferenceStorage;
import org.bugstat.exporters.ExportException;
import org.bugstat.exporters.ProductReferenceStorage;
import org.bugstat.exporters.RecordStorage;
import org.bugstat.exporters.UIStrings;
import org.bugstat.exporters.UserReferenceStorage;
import org.bugstat.exporters.systems.BugExportContext;
import org.bugstat.exporters.systems.BugExporterBase;

/**
 * This class represents bug exporter from Bugzilla tracking system. It is able to retrieve information
 * about changed bugs from Bugzilla and convert it into bug temporal info.
 *
 * @author Basil Shikin
 *
 */
@CanExportBugs
public class BugzillaExporter
    extends BugExporterBase
{
    private static Log log = LogFactory.getLog( BugzillaExporter.class );
    
    private Connection        connection;
    private PreparedStatement retrieveNewBugs;
    private PreparedStatement retrieveChanges;
    
    private Map<Integer, BugzillaFieldType> fields        = new HashMap<Integer, BugzillaFieldType>();
    private Map<Integer, Short>             commentCounts = new HashMap<Integer, Short>();
    private Map<Integer, Product>           products      = new HashMap<Integer, Product>();
    private Map<Integer, Component>         components    = new HashMap<Integer, Component>();
    private Map<Integer, User>              usersById      = new HashMap<Integer, User>();
    private Map<String, User>               usersByEmail   = new HashMap<String, User>();

    @Override
    public String getBugTrackerName()
    {
        return UIStrings.TEXT_BUGZILLA;
    }

    public Date retrieveFirstBugDate() throws ExportException
    {
        Connection        connection    = null;        
        PreparedStatement firstBugSQL = null;
        ResultSet         result        = null;
        try
        {
            connection = connectionManager.retrieveConnection();
            
            firstBugSQL = connection.prepareStatement("SELECT creation_ts FROM bugs WHERE bug_id = 1");
            result = firstBugSQL.executeQuery();
            
            if ( result.next() )
            {
                java.sql.Date sqlDate = result.getDate( "creation_ts" );
                
                return new Date( sqlDate.getTime() );
            }
            else
            {
                throw new RuntimeException("No first bug found");
            }
        }
        catch ( SQLException e )
        {
            log.error( "Unable to retrieve bug first date", e );
            
            throw new ExportException( e );
        }
        finally
        {
            connectionManager.closeStatement( firstBugSQL );
            connectionManager.closeResult( result );
            connectionManager.closeConnection( connection );
        }
    }

    @Override
    protected void populateBugChanges(Date fromDate, Date toDate, RecordStorage recordStorage, BugExportContext context) throws SQLException, IOException
    {
        // First, add all bugs created in that time period
        ResultSet result = null;
        try
        {
            retrieveNewBugs.setObject( 1, fromDate); 
            retrieveNewBugs.setObject( 2, toDate);
            
            result = retrieveNewBugs.executeQuery();
            
            while ( result.next() )
            {
                processNewBug( result, recordStorage, context );
            }
        }
        finally
        {
            connectionManager.closeResult( result );
        }
        
        // Second, process bug changes
        try
        {
            retrieveChanges.setObject( 1, fromDate); 
            retrieveChanges.setObject( 2, toDate);
            
            result = retrieveChanges.executeQuery();
            
            while ( result.next() )
            {
                processBugChange( result, recordStorage, context );
            }
        }
        finally
        {
            connectionManager.closeResult( result );
        }
    }

    @Override
    protected void loadComponents(ComponentReferenceStorage storage) throws SQLException
    {
        super.loadComponents(storage);
        
        PreparedStatement componentsSQL = null;
        ResultSet         result        = null;
        try
        {
            componentsSQL = connection.prepareStatement("SELECT id, name FROM components");
            result = componentsSQL.executeQuery();
            
            while ( result.next() )
            {
                final int componentId = result.getInt( "id" );
                final Component component = new Component( result.getString("name") );
                
                components.put( componentId, component );
                
                storage.addObject( component );
            }
        }
        finally
        {
            connectionManager.closeStatement( componentsSQL );
            connectionManager.closeResult( result );
        }
    }

    @Override
    protected void loadProducts(ProductReferenceStorage storage) throws SQLException
    {
        super.loadProducts(storage);
        
        PreparedStatement componentsSQL = null;
        ResultSet         result        = null;
        try
        {
            componentsSQL = connection.prepareStatement("SELECT id, name FROM products");
            result = componentsSQL.executeQuery();
            
            while ( result.next() )
            {
                final int productId = result.getInt( "id" );
                final Product product = new Product( result.getString("name") );
                
                products.put( productId, product );
                
                storage.addObject( product );
            }
        }
        finally
        {
            connectionManager.closeStatement( componentsSQL );
            connectionManager.closeResult( result );
        }
    }

    @Override
    protected void loadUsers(UserReferenceStorage storage) throws SQLException
    {
        super.loadUsers(storage);
        
        PreparedStatement componentsSQL = null;
        ResultSet         result        = null;
        try
        {
            componentsSQL = connection.prepareStatement("SELECT userid, login_name, realname FROM profiles");
            result = componentsSQL.executeQuery();
            
            while ( result.next() )
            {
                final int userId = result.getInt( "userid" );
                final String userEmail = result.getString( "login_name" );
                final User user = new User( result.getString("realname") );
                
                usersById.put( userId, user );
                usersByEmail.put( userEmail, user );
                
                storage.addObject( user );
            }
        }
        finally
        {
            connectionManager.closeStatement( componentsSQL );
            connectionManager.closeResult( result );
        }
    }

    @Override
    protected void initializeExporter() throws SQLException
    {
        super.initializeExporter();
        
        connection = connectionManager.retrieveConnection();
        
        retrieveNewBugs = connection.prepareStatement( "SELECT bug_id, reporter, creation_ts, assigned_to, bug_severity, bug_status, op_sys, priority, product_id, component_id, resolution " +
        		                                       "FROM bugs " +
        		                                       "WHERE creation_ts >= ? AND creation_ts < ? " +
        		                                       "ORDER BY bug_id" );
        
        retrieveChanges = connection.prepareStatement( "SELECT bug_id, fieldid, added " + 
                                                       "FROM bugs_activity " +
                                                       "WHERE bug_when >= ? AND bug_when < ?" ); 
        
        loadFieldTypes();
        
        commentCounts.clear();
    }

    @Override
    protected void shutdownExporter()
    {
        connectionManager.closeStatement( retrieveNewBugs );
        connectionManager.closeStatement( retrieveChanges );
        
        connectionManager.closeConnection( connection );
        
        super.shutdownExporter();
    }
    
    private void processNewBug(ResultSet result, RecordStorage recordStorage, BugExportContext context) throws IOException, SQLException
    {
        final BugTemporalInfoWriter infoWriter = context.getTempWriter();
        final BugPermanentInfoWriter permWriter = context.getPermWriter();
        
        int bugID = result.getInt( "bug_id" );
        infoWriter.appendBugInfo(bugID, recordStorage);
        
        log.debug( "Processing new Bugzilla bug " + bugID + "...");

        // Populate permanent information about the bug
        permWriter.appendBugInfo( bugID );
        
        User reporter = resolveUserById( result.getString( "reporter" ) );
        permWriter.writeCreatedBy( bugID, reporter);
        
        Date createdAt = result.getDate( "creation_ts" );
        permWriter.writeCreatedDate( bugID, new java.util.Date( createdAt.getTime() ) );
        
        // Populate temporary information about the bug
        infoWriter.writeStatus(bugID, recordStorage, Status.OPEN ); // New bugs are always open
        infoWriter.writeResolution(bugID, recordStorage, Resolution.NONE ); // New bugs are not resolved

        User user = resolveUserById( result.getString( BugzillaFieldType.ASSIGNED_TO.getFieldName() ) );
        infoWriter.writeAssignedTo(bugID, recordStorage, user );
        
        Severity severity = resolveSeverity( result.getString( BugzillaFieldType.SEVERITY.getFieldName() ) );
        infoWriter.writeSeverity(bugID, recordStorage, severity );
        
        Priority priority = resolvePriority( result.getString(  BugzillaFieldType.PRIORITY.getFieldName() ) );
        infoWriter.writePriority(bugID, recordStorage, priority );
        
        Platform platform = resolvePlatform( result.getString( BugzillaFieldType.PLATFORM.getFieldName() ) );
        infoWriter.writePlatform(bugID, recordStorage, platform );
        
        Product product = resolveProduct( result.getString( BugzillaFieldType.PRODUCT.getFieldName() ) );
        infoWriter.writeProduct(bugID, recordStorage, product );
        
        Component component = resolveComponent( result.getString( BugzillaFieldType.COMPONENT.getFieldName() ) );
        infoWriter.writeComponent(bugID, recordStorage, component );
        
        infoWriter.writeCommentsNumber(bugID, recordStorage, (short)1 ); // New bug has 1 comment (description)
        commentCounts.put(bugID, (short)1 ); 
    }
    
    private void processBugChange(ResultSet result, RecordStorage recordStorage, BugExportContext context) throws SQLException, IOException
    {
        BugTemporalInfoWriter infoWriter = context.getTempWriter();
        
        final int bugID       = result.getInt( "bug_id" );
        final String newValue = result.getString( "added" );
        
        log.debug( "Processing change to Bugzilla bug " + bugID + "...");
        
        final BugzillaFieldType changedField = fields.get( result.getInt( "fieldid" ) );
        
        if ( changedField == BugzillaFieldType.ASSIGNED_TO )
        {
            User newUser = resolveUserByEmail( newValue );
            infoWriter.writeAssignedTo(bugID, recordStorage, newUser );
        }
        else if ( changedField == BugzillaFieldType.STATUS )
        {
            Status newStatus = resolveStatus( newValue );
            infoWriter.writeStatus(bugID, recordStorage, newStatus );
        }
        else if ( changedField == BugzillaFieldType.RESOLUTION )
        {
            Resolution newResolution = resolveResolution( newValue );
            infoWriter.writeResolution(bugID, recordStorage, newResolution );
        }
        else if ( changedField == BugzillaFieldType.SEVERITY )
        {
            Severity newSeverity = resolveSeverity( newValue );
            infoWriter.writeSeverity(bugID, recordStorage, newSeverity );
        }
        else if ( changedField == BugzillaFieldType.PRIORITY )
        {
            Priority newPriority = resolvePriority( newValue );
            infoWriter.writePriority(bugID, recordStorage, newPriority );
        }
        else if ( changedField == BugzillaFieldType.PLATFORM )
        {
            Platform newPlatform = resolvePlatform( newValue );
            infoWriter.writePlatform(bugID, recordStorage, newPlatform );
        }
        else if ( changedField == BugzillaFieldType.PRODUCT )
        {
            Product newUser = resolveProduct( newValue );
            infoWriter.writeProduct(bugID, recordStorage, newUser );
        }
        else if ( changedField == BugzillaFieldType.COMPONENT )
        {
            Component newComponent = resolveComponent( newValue );
            infoWriter.writeComponent(bugID, recordStorage, newComponent );
        }
        else if ( changedField == BugzillaFieldType.COMMENT )
        {
            Short count = commentCounts.get( bugID );
            count = (short)(count != null ? count + 1 : 1);
            commentCounts.put(bugID, count );
            
            infoWriter.writeCommentsNumber(bugID, recordStorage, count );
        }
    }
    
    private void loadFieldTypes() throws SQLException
    {
        PreparedStatement componentsSQL = null;
        ResultSet         result        = null;
        try
        {
            componentsSQL = connection.prepareStatement("SELECT id, name FROM fielddefs");
            result = componentsSQL.executeQuery();
            
            while ( result.next() )
            {
                final int    fieldId   = result.getInt( "id" );
                final String fieldName = result.getString( "name" );
                
                BugzillaFieldType type = null; 
                for( BugzillaFieldType possibleMatch : BugzillaFieldType.values() )
                {
                    if ( possibleMatch.getFieldName().equals( fieldName ) )
                    {
                        type = possibleMatch;
                        break;
                    }
                }
                
                if ( type != null )
                {
                    fields.put( fieldId, type );
                }
            }
        }
        finally
        {
            connectionManager.closeStatement( componentsSQL );
            connectionManager.closeResult( result );
        }
    }

    private Component resolveComponent(String newValue)
    {
        try
        {
            return components.get( Integer.valueOf( newValue ) );
        }
        catch ( Exception e )
        {
            return null;
        }
    }

    private Product resolveProduct(String newValue)
    {
        try
        {
            return products.get( Integer.valueOf( newValue ) );
        }
        catch ( Exception e )
        {
            return null;
        }
    }

    private User resolveUserById(String newValue)
    {
        try
        {
            return usersById.get( Integer.valueOf( newValue ) );
        }
        catch ( Exception e )
        {
            return null;
        }
    }
    
    private User resolveUserByEmail(String newValue)
    {
        User result = usersByEmail.get( newValue );
        if ( result == null )
        {
            result = new User("Unknown");
        }
        
        return result;
    }
    
    private Priority resolvePriority(String newValue)
    {
        if ( "P0".equals( newValue ) )
        {
            return Priority.P0;
        }
        else if ( "P1".equals( newValue ) )
        {
            return Priority.P1;
        }
        else if ( "P2".equals( newValue ) )
        {
            return Priority.P2;
        }
        else if ( "P3".equals( newValue ) )
        {
            return Priority.P3;
        }
        else if ( "P4".equals( newValue ) )
        {
            return Priority.P4;
        }
        else if ( "P5".equals( newValue ) )
        {
            return Priority.P5;
        }
        else
        {
            return Priority.NOT_SUPPORTED;
        }
    }

    private Severity resolveSeverity(String newValue)
    {
        if ( "enhancement".equals( newValue ) )
        {
            return Severity.ENHANCEMENT;
        }
        else if ( "trivial".equals( newValue ) || "minor".equals( newValue ) )
        {
            return Severity.MINOR;
        }
        else if ( "normal".equals( newValue ) )
        {
            return Severity.NORMAL;
        }
        else if ( "major".equals( newValue ) )
        {
            return Severity.MAJOR;
        }
        else if ( "critical".equals( newValue ) )
        {
            return Severity.CRITICAL;
        }
        else if ( "blocker".equals( newValue ) )
        {
            return Severity.BLOCKER;
        }
        else
        {
            return Severity.NOT_SUPPORTED;
        }
    }

    private Resolution resolveResolution(String newValue)
    {
        if ( "FIXED".equals( newValue ) )
        {
            return Resolution.FIXED;
        }
        else if ( "INVALID".equals( newValue ) )
        {
            return Resolution.INVALID;
        }
        else if ( "DUPLICATE".equals( newValue ) )
        {
            return Resolution.DUPLICATE;
        }
        else if ( "WORKSFORME".equals( newValue ) )
        {
            return Resolution.WORKSFORME;
        }
        else if ( "WONTFIX".equals( newValue ) )
        {
            return Resolution.WONTFIX;
        }
        else
        {
            return Resolution.NOT_SUPPORTED;
        }
    }

    private Status resolveStatus(String newValue)
    {
        if ( "NEW".equals( newValue ) || "ASSIGNED".equals( newValue ) )
        {
            return Status.OPEN;
        }
        else 
        {
            return Status.CLOSED;
        }
    }
    
    private Platform resolvePlatform(String newValue)
    {
        if ( newValue == null ) return Platform.NOT_SUPPORTED;
        
        final String normalized = newValue.toLowerCase();
        
        if ( normalized.startsWith( "win")  )
        {
            return Platform.WIN;
        }
        else if ( normalized.startsWith( "lin")  )
        {
            return Platform.LINUX;
        }
        else if ( normalized.startsWith( "mac") || normalized.startsWith( "os" ) )
        {
            return Platform.OS_X;
        }
        else
        {
            return Platform.OTHER;
        }
    }

}
